/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.properties;

import java.util.Collections;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Provides access to properties.
 *
 * <p>It is important to understand that usage of this class is different from what you might be
 * used to in java.util.Properties. Specifically, when you get a Properties property, if that
 * property is not set, the return value is NULL. However, when you call the basic getters here, if
 * the property is not set, a RuntimeException is thrown. These methods will never return null
 * (except if you pass in null as the default return value for the methods that take a default).
 *
 * <p>There are methods to get properties as various primitive types, int, double, float, etc. When
 * you invoke one of these methods on a property that is found but cannot be parsed as your desired
 * type, a RuntimeException is thrown.
 *
 * <p>There are corresponding methods which take as a second parameter a default value. These
 * methods, instead of throwing a RuntimeException when the property cannot be found, return the
 * default value. You can use the default value "null" to invoke getProperty() with semantics more
 * like the java.util.Properties object. These augmented accessors which take defaults will be, I
 * hope, especially useful in static initializers. Providing a default in your static initializer
 * will keep your class from blowing up at initialization when your property cannot be found. This
 * seems especially advantageous when there is a plausible default value.
 *
 * <p>This class has a comprehensive JUnit testcase. Please keep the testcase up to date with any
 * changes you make to this class.
 *
 * @since 2.4, this class existed in the main package since uPortal 2.0
 */
public class PropertiesManager {

    protected static final Log log = LogFactory.getLog(PropertiesManager.class);

    public static final String PORTAL_PROPERTIES_FILE_SYSTEM_VARIABLE = "portal.properties";
    private static final String PORTAL_PROPERTIES_FILE_NAME = "/properties/portal.properties";
    private static Properties props = null;

    /**
     * A set of the names of properties that clients of this class attempt to access but which were
     * not set in the properties file. This Set allows this class to report about missing properties
     * and to log each missing property only the first time it is requested.
     */
    private static final Set<String> MISSING_PROPERTIES =
            Collections.synchronizedSet(new HashSet<>());

    /**
     * Setter method to set the underlying Properties. This is a public method to allow poor-man's
     * static dependency injection of the Properties from wherever you want to get them. If
     * Properties have not been injected before any accessor method is invoked, PropertiesManager
     * will invoke loadProperties() to attempt to load its own properties. You might call this from
     * a context listener, say. If Properties have already been loaded or injected, this method will
     * overwrite them.
     *
     * @param props - Properties to be injected.
     */
    public static synchronized void setProperties(Properties props) {
        PropertiesManager.props = props;
    }

    /**
     * Load up the portal properties. Right now the portal properties is a simple .properties file
     * with name value pairs. It may evolve to become an XML file later on.
     */
    protected static void loadProps() {
        PropertiesManager.props = new Properties();
        try {
            String pfile = System.getProperty(PORTAL_PROPERTIES_FILE_SYSTEM_VARIABLE);
            if (pfile == null) {
                pfile = PORTAL_PROPERTIES_FILE_NAME;
            }
            PropertiesManager.props.load(PropertiesManager.class.getResourceAsStream(pfile));
        } catch (Throwable t) {
            log.error("Unable to read portal.properties file.", t);
        }
    }

    /**
     * Returns the value of a property for a given name. Any whitespace is trimmed off the beginning
     * and end of the property value. Note that this method will never return null. If the requested
     * property cannot be found, this method throws an UndeclaredPortalException.
     *
     * @param name the name of the requested property
     * @return value the value of the property matching the requested name
     * @throws MissingPropertyException - if the requested property cannot be found
     */
    public static String getProperty(String name) throws MissingPropertyException {
        if (log.isTraceEnabled()) {
            log.trace("entering getProperty(" + name + ")");
        }
        if (PropertiesManager.props == null) loadProps();
        String val = getPropertyUntrimmed(name);
        val = val.trim();
        if (log.isTraceEnabled()) {
            log.trace("returning from getProperty(" + name + ") with return value [" + val + "]");
        }
        return val;
    }

    /**
     * Returns the value of a property for a given name including whitespace trailing the property
     * value, but not including whitespace leading the property value. An UndeclaredPortalException
     * is thrown if the property cannot be found. This method will never return null.
     *
     * @param name the name of the requested property
     * @return value the value of the property matching the requested name
     * @throws MissingPropertyException - (undeclared) if the requested property is not found
     */
    public static String getPropertyUntrimmed(String name) throws MissingPropertyException {
        if (PropertiesManager.props == null) loadProps();

        if (props == null) {
            boolean alreadyReported = registerMissingProperty(name);
            throw new MissingPropertyException(name, alreadyReported);
        }

        String val = props.getProperty(name);
        if (val == null) {
            boolean alreadyReported = registerMissingProperty(name);
            throw new MissingPropertyException(name, alreadyReported);
        }
        return val;
    }

    /**
     * Returns the value of a property for a given name. This method can be used if the property is
     * boolean in nature and you want to make sure that <code>true</code> is returned if the
     * property is set to "true", "yes", "y", or "on" (regardless of case), and <code>false</code>
     * is returned in all other cases.
     *
     * @param name the name of the requested property
     * @return value <code>true</code> if property is set to "true", "yes", "y", or "on" regardless
     *     of case, otherwise <code>false</code>
     * @throws MissingPropertyException - when no property of the given name is declared.
     */
    public static boolean getPropertyAsBoolean(String name) throws MissingPropertyException {
        if (PropertiesManager.props == null) loadProps();
        boolean retValue = false;
        String value = getProperty(name);
        if (value != null) {
            if (value.equalsIgnoreCase("true")
                    || value.equalsIgnoreCase("yes")
                    || value.equalsIgnoreCase("y")
                    || value.equalsIgnoreCase("on")) {
                retValue = true;
            } else if (value.equalsIgnoreCase("false")
                    || value.equalsIgnoreCase("no")
                    || value.equalsIgnoreCase("n")
                    || value.equalsIgnoreCase("off")) {
                retValue = false;
            } else {
                // this method's historical behavior, maintained here, is to return false
                // for all values that did not match on of the true values above.
                log.error(
                        "property ["
                                + name
                                + "] is being accessed as a boolean "
                                + "but had non-canonical value ["
                                + value
                                + "].  Returning it as false, "
                                + "but this may be a property misconfiguration.");
            }
        } else {
            log.fatal(
                    "property ["
                            + name
                            + "] is being accessed as a boolean "
                            + "but was null.  Returning false.  However, it should not have been "
                            + "possible to get here because getProperty() throws a runtime "
                            + "exception or returns a non-null value.");
        }
        return retValue;
    }

    /**
     * Returns the value of a property for a given name as a <code>byte</code>
     *
     * @param name the name of the requested property
     * @return value the property's value as a <code>byte</code>
     * @throws MissingPropertyException - if the property is not set
     * @throws BadPropertyException - if the property cannot be parsed as a byte
     */
    public static byte getPropertyAsByte(String name)
            throws MissingPropertyException, BadPropertyException {
        if (PropertiesManager.props == null) loadProps();
        try {
            return Byte.parseByte(getProperty(name));
        } catch (NumberFormatException nfe) {
            throw new BadPropertyException(name, getProperty(name), "byte");
        }
    }

    /**
     * Returns the value of a property for a given name as a <code>short</code>
     *
     * @param name the name of the requested property
     * @return value the property's value as a <code>short</code>
     * @throws MissingPropertyException - if the property is not set
     * @throws BadPropertyException - if the property cannot be parsed as a short or is not set.
     */
    public static short getPropertyAsShort(String name)
            throws MissingPropertyException, BadPropertyException {
        if (PropertiesManager.props == null) loadProps();
        try {
            return Short.parseShort(getProperty(name));
        } catch (NumberFormatException nfe) {
            throw new BadPropertyException(name, getProperty(name), "short");
        }
    }

    /**
     * Returns the value of a property for a given name as an <code>int</code>
     *
     * @param name the name of the requested property
     * @return value the property's value as an <code>int</code>
     * @throws MissingPropertyException - if the property is not set
     * @throws BadPropertyException - if the property cannot be parsed as an int
     */
    public static int getPropertyAsInt(String name)
            throws MissingPropertyException, BadPropertyException {
        if (PropertiesManager.props == null) loadProps();
        try {
            return Integer.parseInt(getProperty(name));
        } catch (NumberFormatException nfe) {
            throw new BadPropertyException(name, getProperty(name), "int");
        }
    }

    /**
     * Returns the value of a property for a given name as a <code>long</code>
     *
     * @param name the name of the requested property
     * @return value the property's value as a <code>long</code>
     * @throws MissingPropertyException - if the property is not set
     * @throws BadPropertyException - if the property cannot be parsed as a long
     */
    public static long getPropertyAsLong(String name)
            throws MissingPropertyException, BadPropertyException {
        if (PropertiesManager.props == null) loadProps();
        try {
            return Long.parseLong(getProperty(name));
        } catch (NumberFormatException nfe) {
            throw new BadPropertyException(name, getProperty(name), "long");
        }
    }

    /**
     * Returns the value of a property for a given name as a <code>float</code>
     *
     * @param name the name of the requested property
     * @return value the property's value as a <code>float</code>
     * @throws MissingPropertyException - if the property is not set
     * @throws BadPropertyException - if the property cannot be parsed as a float
     */
    public static float getPropertyAsFloat(String name)
            throws MissingPropertyException, BadPropertyException {
        if (PropertiesManager.props == null) loadProps();
        try {
            return Float.parseFloat(getProperty(name));
        } catch (NumberFormatException nfe) {
            throw new BadPropertyException(name, getProperty(name), "float");
        }
    }

    /**
     * Returns the value of a property for a given name as a <code>long</code>
     *
     * @param name the name of the requested property
     * @return value the property's value as a <code>double</code>
     * @throws MissingPropertyException - if the property has not been set
     * @throws BadPropertyException - if the property cannot be parsed as a double or is not set.
     */
    public static double getPropertyAsDouble(String name)
            throws MissingPropertyException, BadPropertyException {
        if (PropertiesManager.props == null) loadProps();
        try {
            return Double.parseDouble(getProperty(name));
        } catch (NumberFormatException nfe) {
            throw new BadPropertyException(name, getProperty(name), "double");
        }
    }

    /**
     * Registers that a given property was sought but not found. Currently adds the property to the
     * set of missing properties and logs if this is the first time the property has been requested.
     *
     * @param name - the name of the missing property
     * @return true if the property was previously registered, false otherwise
     */
    private static boolean registerMissingProperty(String name) {
        final boolean previouslyReported = !PropertiesManager.MISSING_PROPERTIES.add(name);

        if (!previouslyReported && log.isInfoEnabled()) {
            log.info("Property [" + name + "] was requested but not found.");
        }
        return previouslyReported;
    }

    /**
     * Get the value of the property with the given name. If the named property is not found,
     * returns the supplied default value. This error handling behavior makes this method attractive
     * for use in static initializers.
     *
     * @param name - the name of the property to be retrieved.
     * @param defaultValue - a fallback default value which will be returned if the property cannot
     *     be found.
     * @return the value of the requested property, or the supplied default value if the named
     *     property cannot be found.
     * @since 2.4
     */
    public static String getProperty(String name, String defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        String returnValue = defaultValue;
        try {
            returnValue = getProperty(name);
        } catch (MissingPropertyException mpe) {
            // Do nothing, since we have already recorded and logged the missing property.
        }
        return returnValue;
    }

    /**
     * Get a property as a boolean, specifying a default value. If for any reason we are unable to
     * lookup the desired property, this method returns the supplied default value. This error
     * handling behavior makes this method suitable for calling from static initializers.
     *
     * @param name - the name of the property to be accessed
     * @param defaultValue - default value that will be returned in the event of any error
     * @return the looked up property value, or the defaultValue if any problem.
     * @since 2.4
     */
    public static boolean getPropertyAsBoolean(final String name, final boolean defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        boolean returnValue = defaultValue;
        try {
            returnValue = getPropertyAsBoolean(name);
        } catch (MissingPropertyException mpe) {
            // do nothing, since we already logged the missing property
        }
        return returnValue;
    }

    /**
     * Get the value of the given property as a byte, specifying a fallback default value. If for
     * any reason we are unable to lookup the desired property, this method returns the supplied
     * default value. This error handling behavior makes this method suitable for calling from
     * static initializers.
     *
     * @param name - the name of the property to be accessed
     * @param defaultValue - the default value that will be returned in the event of any error
     * @return the looked up property value, or the defaultValue if any problem.
     * @since 2.4
     */
    public static byte getPropertyAsByte(final String name, final byte defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        byte returnValue = defaultValue;
        try {
            returnValue = getPropertyAsByte(name);
        } catch (Throwable t) {
            log.error(
                    "Could not retrieve or parse as byte property ["
                            + name
                            + "], defaulting to ["
                            + defaultValue
                            + "]",
                    t);
        }
        return returnValue;
    }

    /**
     * Returns the value of a property for a given name as a short. If for any reason the property
     * cannot be looked up as a short, returns the supplied default value. This error handling makes
     * this method a good choice for static initializer calls.
     *
     * @param name - the name of the requested property
     * @param defaultValue - a default value that will be returned in the event of any error
     * @return the property value as a short or the default value in the event of any error
     * @since 2.4
     */
    public static short getPropertyAsShort(String name, short defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        short returnValue = defaultValue;
        try {
            returnValue = getPropertyAsShort(name);
        } catch (Throwable t) {
            log.error(
                    "Could not retrieve or parse as short property ["
                            + name
                            + "], defaulting to given value ["
                            + defaultValue
                            + "]",
                    t);
        }
        return returnValue;
    }

    /**
     * Get the value of a given property as an int. If for any reason the property cannot be looked
     * up as an int, returns the supplied default value. This error handling makes this method a
     * good choice for static initializer calls.
     *
     * @param name - the name of the requested property
     * @param defaultValue - a fallback default value for the property
     * @return the value of the property as an int, or the supplied default value in the event of
     *     any problem.
     * @since 2.4
     */
    public static int getPropertyAsInt(String name, int defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        int returnValue = defaultValue;
        try {
            returnValue = getPropertyAsInt(name);
        } catch (Throwable t) {
            log.error(
                    "Could not retrieve or parse as int the property ["
                            + name
                            + "], defaulting to "
                            + defaultValue,
                    t);
        }
        return returnValue;
    }

    /**
     * Get the value of the given property as a long. If for any reason the property cannot be
     * looked up as a long, returns the supplied default value. This error handling makes this
     * method a good choice for static initializer calls.
     *
     * @param name - the name of the requested property
     * @param defaultValue - a fallback default value that will be returned if there is any problem
     * @return the value of the property as a long, or the supplied default value if there is any
     *     problem.
     * @since 2.4
     */
    public static long getPropertyAsLong(String name, long defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        long returnValue = defaultValue;
        try {
            returnValue = getPropertyAsLong(name);
        } catch (Throwable t) {
            log.error(
                    "Could not retrieve or parse as long property ["
                            + name
                            + "], defaulting to "
                            + defaultValue,
                    t);
        }
        return returnValue;
    }

    /**
     * Get the value of the given property as a float. If for any reason the property cannot be
     * looked up as a float, returns the supplied default value. This error handling makes this
     * method a good choice for static initializer calls.
     *
     * @param name - the name of the requested property
     * @param defaultValue - a fallback default value that will be returned if there is any problem
     * @return the value of the property as a float, or the supplied default value if there is any
     *     problem.
     * @since 2.4
     */
    public static float getPropertyAsFloat(String name, float defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        float returnValue = defaultValue;
        try {
            returnValue = getPropertyAsFloat(name);
        } catch (Throwable t) {
            log.error(
                    "Could not retrieve or parse as float property ["
                            + name
                            + "], defaulting to "
                            + defaultValue,
                    t);
        }
        return returnValue;
    }

    /**
     * Get the value of the given property as a double. If for any reason the property cannot be
     * looked up as a double, returns the specified default value. This error handling makes this
     * method a good choice for static initializer calls.
     *
     * @param name - the name of the requested property
     * @param defaultValue - a fallback default value that will be returned if there is any problem
     * @return the value of the property as a double, or the supplied default value if there is any
     *     problem.
     * @since 2.4
     */
    public static double getPropertyAsDouble(String name, double defaultValue) {
        if (PropertiesManager.props == null) loadProps();
        double returnValue = defaultValue;
        try {
            returnValue = getPropertyAsDouble(name);
        } catch (Throwable t) {
            log.error(
                    "Could not retrieve or parse as double property ["
                            + name
                            + "], defaulting to "
                            + defaultValue,
                    t);
        }
        return returnValue;
    }

    /**
     * Get a Set of the names of properties that have been requested but were not set.
     *
     * @return a Set of the String names of missing properties.
     * @since 2.4
     */
    public static Set getMissingProperties() {
        return PropertiesManager.MISSING_PROPERTIES;
    }
}
